iT邦幫忙

2023 iThome 鐵人賽

DAY 26
1
Modern Web

了不起的 Svelte系列 第 26

第 26 天:Svelte 的互動百寶箱:Store(三)

  • 分享至 

  • xImage
  •  

第 26 天:Svelte 的互動百寶箱:Store(三)

「恩,就是這麼一回事了。我漸漸離我的抱負越來越遠,漸漸在她的愛中沉的越來越深,然後突然之間我什麼都不在意了。在應用程式當中觸發各種事件又有什麼用呢?還不如好好地跳一些通知給她,告訴她我又觸發了哪些事件。」

~節錄自《The Great Svelte:第八章》

第 26 天要講的事

  1. 動手練習 Store
  2. App.svelte 取得 toastStore 的資料
  3. Palettes.svelte 取得 toastStore 的資料

  經過兩天的介紹,對於 Svelte 提供的 Store 物件應該有初步的概念了。今天就讓我們試著在色票產生器這個專案當中來使用 Store,一邊練習一邊加深我們對 Store 的理解吧!

動手練習 Store

  一開始在介紹 Store 的時候,我們說 Store 可以實作出水平傳遞的資料,讓沒有互相隸屬的兩個 Svelte 元件仍然可以透過 Store 來溝通。什麼情況會需要用到這種設計呢?在我們現在色票產生器這個專案裡頭,如果想要實作出一個訊息中心,每當使用者在互動介面上做出新的操作,就跳出訊息來提示使用者。這樣子的一個訊息中心,需要從各個不同的 Svelte 元件獲取事件發生的詳細情況,也就是需要與各種不同的 Svelte 元件進行資料溝通。所以如果想要實作出訊息中心,看起來就適合使用 Store 所提供的水平傳遞資料的特性,做出一個 Store 物件來儲存所有的訊息,並且讓各個不同的 Svelte 都能將各自發生的事件直接傳達到這個 Store 物件當中。
  在現代的網頁設計當中,一則一則隨著事件發生而跳出來提示使用者的訊息有個可愛的暱稱,叫做 Toast。所以我們就來做個 Toast Store 吧。首先在 /src/lib 當中開啟一個新的 Javascript 檔案 toastStore

/src/lib/toastStore.js
import { writable } from "svelte/store";

const createToastStore = () => {
  let uuid = 0;
  const { subscribe, update } = writable([]);

  return {
    subscribe,
    addToast: ({ action, hex }) => {
      const id = uuid++;
      update(state => [...state, { action, hex, id: id }])
      setTimeout(() => update(state => state.filter(x => x.id !== id)), 5000)
    },
    removeToast: ({ id }) => update(state => state.filter(x => x.id !== id)),
  }
}

export const toastStore = createToastStore();
  • 第一行:import { writable } from "svelte/store";
      先引入 writable 這個 Svelte 所提供最基礎萬用的 Store。

  • 第三行:const createToastStore = () => {
      宣告一個函式,createToastStore,讓我們從 writable 這個最基礎的 Store 出發,並包裝成一個適合當作訊息中心的 toast Store。

  • 第四行:let uuid = 0;
      宣告一個變數 uuid,這個變數將會替每一則新加入的訊息標上識別碼。

  • 第五行:const { subscribe, update } = writable([]);
      初始化一個 writable Store。並從中取出我們需要的方法 subscribeupdate

  • 第七行:return {
      回傳一個物件。這個物件將會提供客製化的方式來更新我們的 writable Store。

  • 第八行:subscribe,
      首先原封不動的回傳 subscribe 這個方法。

  • 第九行:addToast: ({ action, hex }) => {
      並且實作出一個新方法 addToast。這個新的方法會需要一個具有 actionhex 兩種資料的物件當作參數,執行的時候會將識別碼 uuid 增加一,藉此達到獨一無二的效果。

  • 第十一行:update(state => [...state, { action, hex, id: id }])
      接著利用 update 將我們的 writable 物件內的資料做更新。更新的方法是加入一個具有 actionhex、以及 id 這三種資料的物件。

  • 第十二行:setTimeout(() => update(state => state.filter(x => x.id !== id)), 5000)
      我們不希望訊息中心跳出來的提示一直顯示在畫面當中,所以經過 5000 毫秒之後,利用 setTimeout 將這個訊息又從 writable 的物件中刪除。

  • 第十四行:removeToast: ({ id }) => update(state => state.filter(x => x.id !== id)),
      實作一個新方法 removeToast。這個方法會需要一個具有 id 資料的物件作為參數,並且依此刪掉 writable 訊息中心相對應的那一則訊息。

  • 第十八行:export const toastStore = createToastStore();
      執行 createToastStore。這樣一來就會得到一個具有 subscribeaddToastremoveToast 這三種方法的物件。由於這三種方法是源自於 writable Store 的 subscribeupdate 變化而來,所以這個新的物件也是一種 Store(其實就是一個包裝過的 writable Store)。而這個新的 Store 當中所儲存的變數會是一個陣列,陣列當中一則一則的物件,具有 actionhex、跟 id,就是我們想要呈現的訊息跟每個訊息的詳細資料。我們把這個新種類的 Store 命名為 toastStore,並且用 export 關鍵字讓其他檔案也能使用。

  沒錯就是這麼簡單,短短十八行就可以做出一間美味可口的 toastStore.js

  並且做一個 Svelte 元件 Toast.svelte 來呈現我們美味的 toast:

/src/lib/Toast.svelte
<script>
  import { toastStore } from "./toastStore";
  import plus from "../assets/plus.svg";
  import minus from "../assets/minus.svg";
  import lock from "../assets/lock.svg";
  import unlock from "../assets/unlock.svg";
  export let toast;

  const icons = { plus, minus, lock, unlock };
  $: icon = icons[toast.action];
</script>

<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
<div class="toast" on:click={() => toastStore.removeToast({ id: toast.id })}>
  <div class="icon">
    <img src={icon} alt={toast.action} />
  </div>
  <div class="hex" style="color: #{toast.hex}">{toast.hex}</div>
</div>

<!-- CSS的部分先略過不呈現,詳細可見文末附錄 -->
  • 第二行:import { toastStore } from "./toastStore";
      引入我們做好的 toastStore

  • 第七行:export let toast;
      宣告一個 Toast.svelte 的 Property toast

  • 第十四行:<div class="toast" on:click={() => toastStore.removeToast({ id: toast.id })}>
      這就是我們一則一則 toast 在 HTML 上面呈現出來的本體。加上一個 on:click 事件處理器,讓使用者可以透過點擊 toast 本體直接點掉這個提示訊息。

App.svelte 取得 toastStore 的資料

  既然有了 toastStore 這個美味的 Store,也有了用來呈現 toastStore 資料的 Svelte 元件 Toast.svelte,那麼我們就來到 App.svelte 使用看看吧:

/src/App.svelte
<script>
  /* 省略與今天主題無關的內容 */
  import Toast from "./lib/Toast.svelte";
  import { toastStore } from "./lib/toastStore";

  const handleClick = (e) => {
    console.log(e);
    const tobeCount = count + e.detail;
    if (0 <= tobeCount && tobeCount < 7) {
      switch (e.detail) {
        case 1:
          palettes = [...palettes, generatePalette()];
          toastStore.addToast({
            action: "plus",
            hex: palettes.slice(-1)[0].hex,
          });
          break;
        case -1:
          toastStore.addToast({
            action: "minus",
            hex: palettes.slice(-1)[0].hex,
          });
          palettes = palettes.slice(0, -1);
          break;
      }
    } else showModal = true;
  };
</script>

<!-- 將 Toast 放在這邊 -->
{#each $toastStore as toast}
  <Toast {toast} />
{/each}

<main>
  <!-- 省略與今天主題無關的內容 -->
</main>

<Modal {showModal} on:closeModal={() => (showModal = false)} />
  • 第三行:import Toast from "./lib/Toast.svelte";
      引入需要的 Svelte 元件 Toast.svelte

  • 第四行:import { toastStore } from "./lib/toastStore";
      從 Javascript 檔案 toastStore.js 當中引入 toastStore 這個 Store 物件。

  • 第十二行:palettes = [...palettes, generatePalette()];
      如果 <Counter /> 當中代表加一的事件發生時,替我們的 palettes 多加入一個色票。

  • 第十三行: toastStore.addToast({
      並且用 addToast 這個方法,將發生的事件 action: "plus" 跟新加入的色碼 hex: palettes.slice(-1)[0].hex 包裝起來儲存到 toastStore 這個 Store 裡面的陣列當中。也就是新增一個「色票加一」的通知。

  • 第十九行:toastStore.addToast({
      如果 <Counter /> 當中代表減一的事件發生時,先將即將發生的事件 action: "minus" 跟準備要被移除的色碼 hex: palettes.slice(-1)[0].hex 包裝起來儲存到 toastStore 這個 Store 裡面的陣列當中。也就是新增一個「色票減一」的通知。

  • 第二十三行:palettes = palettes.slice(0, -1);
      然後就將色票減一。

  • 第三十一行:{#each $toastStore as toast}
      利用第 25 天:Svelte 的互動百寶箱:Store(二)介紹過的自動訂閱 (auto-subscription) 的作法,直接用 $toastStore 取得 toastStore 當中變數的值。因為這個變數是一個陣列,所以使用 {#each} 展開 each 邏輯區塊來迭代陣列當中的各個項目,也就是各個訊息。

  • 第三十二行:<Toast {toast} />
      每一則訊息就做一個 <Toast />,並且將訊息的詳細內容用 toast Property 傳遞給 <Toast />

  • 第三十三行:{/each}
      結束 each 邏輯區塊。

https://i.ibb.co/n0jzPCn/26.gif
圖一、一則一則跳出來的可愛 toast

  以往 Svelte 元件間要互相傳遞資料只能透過 Property,因此在設計檔案架構時,需要共用相同資料的 Svelte 元件必為從屬關係,讓資料可以從父元件 (parent component) 向子元件 (child component) 傳遞,或是透過 bind 讓子元件 (child component) 去修改父元件 (parent) 的資料。現在有了 Store,需要共用相同資料很簡單,只要在 Javascript 的段落中用 import 直接將需要的 Store 引入進來就可以。

Palettes.svelte 取得 toastStore 的資料

  除了在 App.svelte 可以直接獲取並更新 toastStore 當中儲存的變數之外,讓我們也試試看從 Palettes.svelte 來對 toastStore 儲存的資料做改變:

/src/lib/Palettes.svelte
<script>
  import unlock from "../assets/unlock.svg";
  import lock from "../assets/lock.svg";
  import { toastStore } from "./toastStore";
  export let palettes;

  /* 省略與今天主題無關的內容 */
  const handleLock = ({ locked, hex }) =>
    toastStore.addToast({ action: locked ? "lock" : "unlock", hex });
</script>

<div class="palettes">
  <!-- 省略與今天主題無關的內容 -->

      <div class="lock-icon">
        <label>
          <input
            type="checkbox"
            bind:checked={locked}
            on:change={() => handleLock({ locked, hex })}
          />
          {#if locked}
            <img src={lock} alt="color-locked" />
          {:else}
            <img src={unlock} alt="color-unlocked" />
          {/if}
        </label>
      </div>

  <!-- 省略與今天主題無關的內容 -->
</div>
  • 第四行:import { toastStore } from "./toastStore";
      從 toastStore.js 當中引入需要的 toastStore

  • 第八行:const handleLock = ({ locked, hex }) =>
      宣告一個事件處理器 handleLock,用來處理上鎖/解鎖的事件。雖然我們已經用 bind 讓主要元件 App.svelte 當中的變數 palettes 可以直接做改變了,但現在除了 palettes 需要做改變之外,我們也需要改變 toastStore 當中的變數。所以這邊多實作一個事件處理器去完成這項任務。

  • 第九行:toastStore.addToast({ action: locked ? "lock" : "unlock", hex });
      事件處理器的工作很簡單,當更新之後的 locked 狀態為 true,就用 addToast 這個方法新增一個「色票上鎖」的通知。反之,當更新之後的 locked 狀態為 false,就用 addToast 這個方法新增一個「色票解鎖」的通知。

  • 第二十行:on:change={() => handleLock({ locked, hex })}
      並在 <input type="checkbox"> 這個 HTML 元素上放入我們需要的事件處理器 handleLock

https://i.ibb.co/wB0jKpX/26.gif
圖二、我鎖起來了!我又解鎖了!我又鎖起來了!我又又解鎖了!

  那麼這就是今天關於 Store 的實作部分了,詳細的程式碼可以參考文末附錄,或是在 Github 資源庫上也可以找到。謝謝大家。

附錄:Toast.svelte

/src/lib/Toast.svelte
<script>
  import { toastStore } from "./toastStore";
  import plus from "../assets/plus.svg";
  import minus from "../assets/minus.svg";
  import lock from "../assets/lock.svg";
  import unlock from "../assets/unlock.svg";
  export let toast;

  const icons = { plus, minus, lock, unlock };
  $: icon = icons[toast.action];
</script>

<!-- svelte-ignore a11y-click-events-have-key-events a11y-no-static-element-interactions -->
<div class="toast" on:click={() => toastStore.removeToast({ id: toast.id })}>
  <div class="icon">
    <img src={icon} alt={toast.action} />
  </div>
  <div class="hex" style="color: #{toast.hex}">{toast.hex}</div>
</div>

<style>
  .toast {
    font-size: 1.2em;
    font-weight: bold;
    width: 10em;
    padding: 1em 0;
    background: #fff;
    display: flex;
    justify-content: center;
    border-radius: 0.5em;
    gap: 1em;
    position: fixed;
    bottom: 1em;
    left: 50%;
    transform: translateX(-50%);
  }

  .toast .icon {
    width: 1.2em;
  }

  .toast .hex {
    width: 5em;
  }
</style>

上一篇
第 25 天:Svelte 的互動百寶箱:Store(二)
下一篇
第 27 天:Svelte 的過場:`transition`
系列文
了不起的 Svelte30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言